Sync 与 Async Python:有什么区别?

Sync 与 Async Python:有什么区别?

你有没有听人说过异步 Python 代码比“普通”(或同步) Python 代码更快?这怎么可能?在本文中,我将尝试解释什么是异步以及它与普通 Python 代码的区别。

Sync 和 Async 是什么意思?

Web 应用程序通常需要处理许多请求,所有请求都是在短时间内从不同的客户端发出的。为了避免处理延迟,它们必须能够并行处理多个请求(通常称为并发)。在本文中,我将继续使用 web 应用程序作为例子,但请记住,还有其他类型的应用程序也受益于同时完成多个任务,因此这个讨论并不是专门针对 web 的。

术语“ sync”和“ async”指的是编写使用并发的应用程序的两种方式。所谓的“sync”服务器使用线程和进程的底层操作系统支持来实现这种并发。以下是同步部署的效果图:

在这种情况下,我们有五个客户端,所有客户端都向应用程序发送请求。这个应用程序的公共访问点是一个 web 服务器,它充当一个负载均衡器,将请求分发给一组服务器 worker,这些 worker 可以实现为进程、线程或两者的组合。worker 执行负载均衡器分配给他们的请求。你可以用 Flask 或 Django 这样的 web 应用程序框架来编写应用程序逻辑,它们就位于这些 worker 中。

这种类型的解决方案非常适合拥有多个 CPU 的服务器,因为你可以将 worker 的数量配置为 cpu 数量的倍数,并且通过这种配置,实现 cores 的均匀利用率,这是单个 Python 进程无法做到的,因为 全局解释器锁定(GIL) 强加了一些限制。

就缺点而言,上图清楚地表明了此方法的主要局限性。 我们有5个客户端,但只有4个 worker 。 如果这5个客户端同时发送请求,而负载平衡器只能给每个worker 分派 1个请求,没有竞争到 worker 的请求将保留在队列中,等待一个 worker 可用。 因此,5个客户端中有4个将及时收到回复,但其中1个将不得不等待更长的时间。 使服务器性能良好的关键在于选择适当数量的 worker,以防止或尽量减少在给定预期负载的情况下阻塞请求的情况。

异步服务器设置较难绘制,但这是我的最佳选择:

这种类型的服务器在由循环控制的单个进程中运行。 循环是一个非常高效的任务管理器和调度器,它创建任务以处理客户端发送的请求。 与长期运行的服务器 worker 不同,循环会创建一个异步任务来处理特定的请求,当该请求完成时,该任务将被销毁。 在任何给定的时间,异步服务器可能有数百个甚至数千个活动任务,所有这些任务由循环管理并同时完成自己的工作。

你可能想知道异步任务之间的并行性是如何实现的。这是有趣的部分,因为异步应用程序完全依赖于协作式多任务处理。这意味着什么?当一个任务需要等待一个外部事件时,例如来自数据库服务器的响应,而不是像同步 worker 那样等待,它告诉循环需要等待什么,然后将控制权返回给循环。然后循环可以找到另一个准备运行的任务,而此任务被数据库阻塞。最终数据库将发送响应,此时循环将考虑第一个任务准备再次运行,并将尽快恢复它。

异步任务暂停和恢复执行的这种能力比较抽象可能难以理解。为了帮助你将其应用于你可能已经知道的事情,请考虑在 Python 中,实现此目的的一种方法是使用 awaityield 关键字,但这并不是唯一的方法,你稍后将看到。

异步应用程序完全在单个进程和单个线程中运行,这令人惊讶。 当然,这种类型的并发需要一定的规则,因为你不能让任务在 CPU 上停留太长时间,否则剩余的任务就会饿死。 为了使异步工作,所有任务都需要自动暂停并及时将控制权返回给循环。 要从异步风格中受益,应用程序需要执行的任务通常会被 I/O 阻塞,并且不需要太多的CPU工作。 Web应用程序通常非常适合,特别是如果它们需要处理大量客户端请求时。

为了在使用异步服务器时最大限度地利用多个 CPU,通常会创建一个混合解决方案,添加一个负载平衡器并在每个CPU上运行一个异步服务器,如下图所示:

在 Python 中实现异步的两种方法

我确定你知道,要在 Python 中编写异步应用程序,你可以使用 asyncio 包,它构建在协程之上,以实现所有异步应用程序都需要的挂起和恢复特性。关键词 yield,以及更新的 asyncawait,是 asyncio 构建异步功能的基础。为了描绘一幅完整的图景,Python 生态系统中还有其他基于协程的异步解决方案,比如 TrioCurio。还有 Twisted,它是最古老的协同框架,甚至早于 asyncio

如果你有兴趣编写一个异步 web 应用程序,有很多基于 coroutines 的异步框架可供选择,包括 aiohttpsanicFastAPITornado

很多人不知道的是,协程只是 Python 中可用于编写异步代码的两种方法之一。 第二种方法是基于一个名为greenlet 的软件包,你可以使用pip进行安装。 Greenlets与协程类似,因为它们也允许 Python 函数暂停执行并在以后恢复执行,但是实现方式却完全不同,这意味着 Python 中的异步生态系统分为两个大类。

coroutine 和 greenlets 进行异步开发的有趣的区别在于,前者需要 Python 语言的特定关键字和特性才能工作,而后者不需要。我的意思是,基于 coroutine 的应用程序需要使用非常特定的语法编写,而基于 greenlet 的应用程序看起来与普通的 Python 代码完全一样。这非常酷,因为在某些条件下,它允许异步执行同步代码,这是基于 coroutine 的解决方案(例如asyncio)无法做到的。

那么在 greenlet 方面有哪些 asyncio 的等价物呢?我知道三个基于 greenlets 的异步包: Gevent, EventletMeinheld,尽管最后一个更像是一个 web 服务器而不是一个通用的异步库。它们都有自己的异步循环实现,并且提供了一个有趣的“ monkey-patching”特性,用在 greenlets 上实现的等效非阻塞版本替换 Python 标准库中的阻塞函数,比如那些执行网络和线程的函数。如果你有一段希望异步运行的同步代码,那么这些包很有可能会让你做到这一点。

你会对此感到惊讶的。据我所知,唯一明确支持 greenlets 的 web 框架是 Flask。此框架会自动检测你何时在greenlet web 服务器上运行,并进行相应的调整,而无需进行任何配置。在执行此操作时,您需要注意不要调用阻塞函数,否则,请使用Monkey-patching来“修复”这些阻塞函数。

但是,Flask 并不是唯一可以从 greenlets 中受益的框架。 其他的 web 框架,比如 Django 和 Bottle,它们不知道 greenlets,当它们与 greenlet web 服务器配对时,也可以异步运行,并且猴子补丁修复了阻塞功能。

异步比同步更快吗?

关于同步和异步应用程序的性能,存在广泛的误解。 人们认为异步应用程序比同步应用程序要快得多。

让我澄清一下,以便我们达成统一认知。 不管 Python 代码是同步编写还是异步编写,它的运行速度都是完全相同的。 除了代码外,还有两个因素可以影响并发应用程序的性能:上下文切换和可伸缩性。

上下文切换

在所有正在运行的任务之间公平地共享 CPU 所需的工作(称为上下文切换)可能会影响应用程序的性能。对于同步应用程序,此工作由操作系统完成,并且基本上是一个没有配置或微调选项的黑匣子。对于异步应用程序,上下文切换由循环完成

asyncio 提供的默认循环实现是用 Python 编写的,它不被认为是非常高效的。 uvloop 软件包提供了一个替代循环,该循环部分用C代码实现,以实现更好的性能。 GeventMeinheld 使用的事件循环也用 C 代码编写。Eventlet使用 Python 编写的循环。

高度优化的异步循环在进行上下文切换方面可能比操作系统更有效,但是以我的经验,要想看到切实的性能提升,你必须在很高的并发级别上运行。对于大多数应用程序,我认为同步和异步上下文切换之间的性能差异不会很显著。

伸缩性

我认为,异步更快的神话来源于异步应用程序通常能够更有效地使用 cpu,因为它们比同步具有更好的伸缩性和更灵活的方法。

考虑一下如果上图所示的同步服务器同时接收100个请求,将会发生什么情况。该服务器一次不能处理超过4个请求,因此其中大多数请求将在队列中等待一段时间,然后才能分配 worker。

与异步服务器相比,异步服务器会立即创建100个任务(如果使用混合模型,4个异步 worker 每个会创建25个任务)。使用异步服务器,所有请求都可以在不必等待的情况下开始处理(不过公平地说,可能还存在其他会降低速度的瓶颈,比如对活动数据库连接数的限制)。

如果这100个任务大量使用 CPU,那么同步和异步解决方案将有类似的性能,因为 CPU 运行的速度是固定的,Python 的执行代码的速度总是相同的,应用程序所做的工作也是相同的。但是,如果任务需要执行大量 I/O 操作,那么只有4个并发请求,同步服务器可能无法实现高 CPU 利用率。另一方面,异步服务器肯定能够更好地保持 cpu 处于忙碌状态,因为它并行地运行所有100个请求。

你可能想知道为什么不能运行100个同步 worker,这样两个服务器就具有相同的并发性。考虑一下,每个 worker都需要有自己的 Python 解释器,以及与之相关的所有资源,再加上具有自己资源应用程序的一个单独副本。服务器和应用程序的大小将决定可以运行多少个 worker 实例,但通常这个数字并不是很高。另一方面,异步任务非常轻量级,并且都在单个 worker 进程的上下文中运行,因此它们具有明显的优势。

记住这些,我们可以说异步只有在以下情况下才会比同步更快:

  • 高负载(没有高负载就没有高并发性的优势)
  • 任务受 I/O 约束(如果任务受CPU约束,那么 超过CPU 数以上的并发性就没有帮助了)
  • 你可以查看每单位时间处理的平均请求数。如果查看单个请求处理时间,你不会看到很大的差异,并且因为有更多的并发任务竞争 CPU,异步甚至可能会稍微慢一点

我希望本文能够澄清一些关于异步代码的混淆和误解。我希望你们记住以下两个要点:

  • 在高负载下,异步应用程序只会比同步等效程序做得更好
  • 由于 greenlet,即使你编写普通代码并使用传统框架(如 Flask 或 Django) ,也可以从异步中受益

如果你想更详细地了解异步系统是如何工作的,请查看我的 PyCon 演示文稿 Asynchronous Python for the Complete Beginner

翻译
Sync vs. Async Python: What is the Difference?

-------------本文结束感谢您的阅读-------------
geekspeng wechat
欢迎您扫一扫上面的二维码,订阅我的微信公众号!
0%